package com.gitee.zycra.file.utils;

import com.gitee.zycra.file.config.ConnectionConfig;
import com.gitee.zycra.file.monitor.SpeedLimitMonitor;
import com.gitee.zycra.file.param.DownloadParam;
import com.gitee.zycra.file.param.UploadParam;
import com.gitee.zycra.file.pool.SessionPool;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.SftpException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Pattern;

/**
 * Util for manage files between local and SFTP server.
 * <p>For business integrity, when all files uploaded success in one directory,
 * <p>an OK check file contains all file names in current directory should be uploaded for record check.
 * <p>The OK file can be writen by {@link #writeOKFile(List, String)}.
 *
 * @author zycra
 * @since 1.0.0
 */
public final class SFTPUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(SFTPUtil.class);

    private static final String DIR_LABEL = "/";

    private static final String DOT = ".";
    private static final String DOTS = "..";

    private SFTPUtil() {
    }

    /**
     * Get a connection from connection pool by connection config.
     *
     * @param connectionConfig connection config
     * @return SFTP connection.
     * @since 1.0.0
     */
    private static ChannelSftp getConnection(ConnectionConfig connectionConfig) {
        try {
            return SessionPool.getPool().borrowObject(connectionConfig);
        } catch (Exception e) {
            LOGGER.info("get connection failed, host={}, userName={}", connectionConfig.getHost(), connectionConfig.getUsername(), e);
        }
        return null;
    }

    /**
     * Return a connection to the connection pool by connection config.
     *
     * @param connectionConfig connection config.
     * @param channelSftp      SFTP connection.
     * @since 1.0.0
     */
    private static void closeConnection(ConnectionConfig connectionConfig, ChannelSftp channelSftp) {
        try {
            SessionPool.getPool().returnObject(connectionConfig, channelSftp);
        } catch (Exception e) {
            LOGGER.info("close connection failed, host={}, userName={}", connectionConfig.getHost(), connectionConfig.getUsername(), e);
        }
    }

    /**
     * Upload file to SFTP server.
     *
     * @param connectionConfig connection config.
     * @param uploadParam      upload param.
     * @return whether successful.
     * @since 1.0.0
     */
    public static boolean upload(ConnectionConfig connectionConfig, UploadParam uploadParam) {
        ChannelSftp channelSftp = getConnection(connectionConfig);
        if (channelSftp == null) {
            return false;
        }
        String localFilePathAndName = FileEncryptUtil.encrypt(uploadParam);
        if (localFilePathAndName == null || localFilePathAndName.isBlank()) {
            LOGGER.error("localFilePathAndName is blank or encrypt file filed");
            return false;
        }
        String serverPath = removeLastDirLabel(uploadParam.getServerPath());
        String serverPathAndFileName = serverPath + DIR_LABEL + uploadParam.getServerFileName();
        CountDownLatch latch = new CountDownLatch(1);
        try {
            mkdirs(channelSftp, serverPath);
            channelSftp.put(localFilePathAndName, serverPathAndFileName, new SpeedLimitMonitor(uploadParam.getSpeedLimit(), latch));
            LOGGER.info("SFTP upload success serverPathAndFileName={}", serverPathAndFileName);
            return true;
        } catch (Exception e) {
            LOGGER.error("SFTP upload failed serverPathAndFileName={}", serverPathAndFileName, e);
            latch.countDown();
        } finally {
            if (uploadParam.isDeleteOriginFile() || uploadParam.isEncrypt()) {
                try {
                    latch.await();
                    Files.delete(new File(localFilePathAndName).toPath());
                } catch (InterruptedException e) {
                    LOGGER.info("wait to delete exception", e);
                    Thread.currentThread().interrupt();
                } catch (IOException e) {
                    LOGGER.error("delete file error", e);
                }
            }
            closeConnection(connectionConfig, channelSftp);
        }
        return false;
    }

    /**
     * Download files from SFTP server.
     *
     * @param connectionConfig conection config.
     * @param downloadParam    download param.
     * @return downloaded file paths in local storage.
     * @since 1.0.0
     */
    public static List<String> download(ConnectionConfig connectionConfig, DownloadParam downloadParam) {
        ChannelSftp channelSftp = getConnection(connectionConfig);
        List<String> result = new ArrayList<>();
        if (channelSftp == null) {
            return result;
        }
        String serverPath = removeLastDirLabel(downloadParam.getServerPath());
        String localPath = removeLastDirLabel(downloadParam.getLocalPath());
        try {
            List<String> fileNameList = filterFileNameList(downloadParam, channelSftp, serverPath);
            if (fileNameList.isEmpty()) {
                LOGGER.warn("fileNameList is empty");
                return result;
            }
            for (String fileName : fileNameList) {
                String localFileName = localPath + DIR_LABEL + fileName;
                if (!CompressUtil.mkdirs(new File(localFileName).getParentFile())) {
                    LOGGER.info("mkdir failed, localFileName={}", localFileName);
                    continue;
                }
                channelSftp.get(serverPath + DIR_LABEL + fileName, localFileName, new SpeedLimitMonitor(downloadParam.getSpeedLimit(), null));
                if (FileEncryptUtil.decrypt(downloadParam, localFileName)) {
                    result.add(localFileName);
                }
            }
        } catch (Exception e) {
            LOGGER.info("SFTP download failed downloadParam={}", downloadParam, e);
            return new ArrayList<>();
        } finally {
            closeConnection(connectionConfig, channelSftp);
        }
        return result;
    }

    /**
     * List all file names in the target directory on SFTP server.
     *
     * @param connectionConfig connection config.
     * @param path             the target directory.
     * @return all file names in the target directory.
     * @throws JSchException connection exception.
     * @since 1.0.0
     */
    public static List<String> ls(ConnectionConfig connectionConfig, String path) throws JSchException {
        ChannelSftp channelSftp = getConnection(connectionConfig);
        if (channelSftp == null) {
            throw new JSchException("get connection failed");
        }
        try {
            return ls(channelSftp, path);
        } finally {
            closeConnection(connectionConfig, channelSftp);
        }
    }

    /**
     * Write OK file by file names.
     *
     * @param fileNameList         the file names to write.
     * @param localFilePathAndName the path to store OK file.
     * @since 1.0.0
     */
    public static void writeOKFile(List<String> fileNameList, String localFilePathAndName) {
        if (fileNameList == null || fileNameList.isEmpty() || localFilePathAndName == null || localFilePathAndName.isBlank()) {
            return;
        }
        File file = new File(localFilePathAndName);
        if (!CompressUtil.mkdirs(file.getParentFile())) {
            LOGGER.info("mkdir failed, localFilePathAndName={}", localFilePathAndName);
            return;
        }
        try (FileWriter fw = new FileWriter(file, StandardCharsets.UTF_8);
             BufferedWriter bw = new BufferedWriter(fw)) {
            for (int i = 0, size = fileNameList.size(); i < size; i++) {
                bw.write(fileNameList.get(i));
                if (i + 1 < size) {
                    bw.newLine();
                }
            }
            bw.flush();
        } catch (Exception e) {
            LOGGER.error("write ok file error", e);
        }
    }

    /**
     * Read OK file by path.
     *
     * @param localFilePathAndName the path to store OK file.
     * @return all file names in OK file.
     * @since 1.0.0
     */
    public static List<String> readOKFile(String localFilePathAndName) {
        List<String> result = new ArrayList<>();
        if (localFilePathAndName == null || localFilePathAndName.isBlank()) {
            LOGGER.error("localFilePathAndName is null");
            return result;
        }
        File file = new File(localFilePathAndName);
        boolean available = Files.isReadable(file.toPath());
        if (!available) {
            LOGGER.error("file not exist or can not read, localFilePathAndName={}", localFilePathAndName);
            return result;
        }
        try (Scanner scanner = new Scanner(file, StandardCharsets.UTF_8)) {
            while (scanner.hasNext()) {
                result.add(scanner.nextLine());
            }
        } catch (Exception e) {
            LOGGER.error("read ok file error", e);
        }
        return result;
    }

    /**
     * List all file names in the target directory on SFTP server.
     *
     * @param channelSftp {@link ChannelSftp} instance.
     * @param path        the target directory.
     * @return all file names in the target directory.
     * @throws JSchException channelSftp exception.
     * @since 1.0.0
     */
    private static List<String> ls(ChannelSftp channelSftp, String path) throws JSchException {
        try {
            List<String> result = new ArrayList<>();
            List<?> list = channelSftp.ls(path);
            for (Object o : list) {
                String fileName = ((ChannelSftp.LsEntry) o).getFilename();
                if (DOT.equals(fileName) || DOTS.equals(fileName)) {
                    continue;
                }
                result.add(fileName);
            }
            return result;
        } catch (SftpException e) {
            if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
                LOGGER.info("SFTP ls no such file", e);
                return new ArrayList<>();
            }
            LOGGER.info("SFTP ls error", e);
            throw new JSchException("SFTP ls error");
        }
    }

    /**
     * Recursively create directorys on SFTP server.
     *
     * @param channelSftp {@link ChannelSftp} instance.
     * @param serverPath  the target directory.
     * @since 1.0.0
     */
    private static void mkdirs(ChannelSftp channelSftp, String serverPath) {
        if (serverPath == null || serverPath.isBlank()) {
            return;
        }
        if (checkDirExists(channelSftp, serverPath)) {
            return;
        }
        if (serverPath.contains(DIR_LABEL)) {
            mkdirs(channelSftp, serverPath.substring(0, serverPath.lastIndexOf(DIR_LABEL)));
        }
        if (checkDirExists(channelSftp, serverPath)) {
            return;
        }
        try {
            channelSftp.mkdir(serverPath);
        } catch (SftpException e) {
            LOGGER.error("SFTP mkdir failed", e);
        }
    }

    /**
     * Check if the target directory exists.
     *
     * @param channelSftp {@link ChannelSftp} instance.
     * @param serverPath  the target directory.
     * @return true if the target directory exists, false otherwise.
     * @since 1.0.0
     */
    private static boolean checkDirExists(ChannelSftp channelSftp, String serverPath) {
        try {
            channelSftp.stat(serverPath);
            return true;
        } catch (SftpException ex) {
            return false;
        }
    }

    /**
     * Remove the last directory label of given path.
     *
     * @param path the path to be removed.
     * @return remove result.
     * @since 1.0.0
     */
    private static String removeLastDirLabel(String path) {
        return path.endsWith(DIR_LABEL) ? path.substring(0, path.length() - 1) : path;
    }

    /**
     * Filter file names by download pattern.
     *
     * @param downloadParam download param.
     * @param channelSftp   {@link ChannelSftp} instance.
     * @param serverPath    the target directory.
     * @return filter result.
     * @throws JSchException connection exception.
     * @since 1.0.0
     */
    private static List<String> filterFileNameList(DownloadParam downloadParam, ChannelSftp channelSftp, String serverPath) throws JSchException {
        List<String> fileNameList = new ArrayList<>();
        String filePattern = downloadParam.getFilePattern();
        if (filePattern != null && !filePattern.isBlank()) {
            Pattern pattern = Pattern.compile(filePattern);
            List<String> list = ls(channelSftp, serverPath);
            for (String fileName : list) {
                if (pattern.matcher(fileName).matches()) {
                    fileNameList.add(fileName);
                }
            }
        }
        List<String> downloadNameList = downloadParam.getFileNameList();
        if (downloadNameList != null && !downloadNameList.isEmpty()) {
            fileNameList.addAll(downloadNameList);
        }
        return fileNameList;
    }
}
